<?php
/**
 * Eka Framework 0.3.0
 *
 * @copyright Copyright (c) 2008 Alexey Yu. Chuprina <656b61@gmail.com> (http://php-eka.googlecode.com)
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * @version $Id$
 */

require_once 'Eka/Db/Expr.php';

/**
 * Eka_Db
 *
 * @package Db
 * @subpackage Adapter
 */
abstract class Eka_Db_Adapter_Abstract
{
	const CONFIG_DBNAME = 'dbname';
	const CONFIG_DRIVER_OPTIONS = 'driver_options'; // for PDO::__construct(..., $driver_options)

	/**
	 * Return Eka_Db_Expr object
	 *
	 * @param strign $expression
	 * @return Eka_Db_Expr
	 */
	static function expr($expression)
	{
		return new Eka_Db_Expr($expression);
	}

	############################################################################

	/**
	 * Symbol the adapter uses for delimited identifiers
	 *
	 * @var strign
	 */
	protected $quoteIdentifierSymbol;

	/**
	 * Database connection
	 *
	 * @var object|resource
	 */
	protected $connection;

	protected $host;
	protected $username;
	protected $password;
	protected $charset;

	protected $config = array();
	
	protected $log = array();
	
	protected $loging = false;

	/**
	 * Constructor
	 *
	 * @param string $host
	 * @param string $username
	 * @param string $password
	 * @param mixed $config DaРµabase name or config array
	 * @param string $charset
	 */
	public function __construct($host, $username, $password, $config = null, $charset =  null)
	{
		if (is_string($config) && $config !== '') {
			$this->config[self::CONFIG_DBNAME] = $config;
		} elseif (is_array($config)) {
			$this->config = $config;
		}

		$this->host = $host;
		$this->username = $username;
		$this->password = $password;
		$this->charset = $charset;
	}

	public function loging($is = null)
	{
		if (null !== $is) {
			$old = $this->loging;
			$this->loging = !!$is;
			return $old;
		}
		return $this->loging;
	}
	
	public function log($sql, $bind)
	{
		if (!$this->loging) {
			return;
		}
		$this->log[] = array($sql, $bind);
	}
	
	public function getLog()
	{
		return $this->log;
	}
	
	/**
	 * @return string
	 */
	public function getCurrentSchema()
	{
		$db = $this->fetchCell('SELECT DATABASE()');
		return $db ? $db : null;
	}

	/**
	 * Get value for {@see $config} by key
	 *
	 * If not defined return null
	 *
	 * @param string $key
	 * @return mixed
	 */
	protected function getConfigValue($key)
	{
		return isset($this->config[$key]) ? $this->config[$key] : null;
	}

	/**
	 * Returns database connection
	 *
	 * If not presently connected, initiates the connection.
	 *
	 * @return object|resource
	 */
	public function getConnection()
	{
		$this->connect();
		return $this->connection;
	}

	/**
	 * Prepares and executes an SQL statement with bound data
	 *
	 * @param  mixed  $sql
	 * @param  mixed  $bind
	 * @return Eka_Db_Statement_Abstract
	 */
	public function query($sql, $bind = null)
	{
		$this->connect();

		if (null !== $bind && !is_array($bind)) {
			$bind = array($bind);
		}
		
		//trace($sql, $bind);
		
		$stmt = $this->prepare($sql);
		$stmt->execute($bind);

		return $stmt;
	}

	/**
	 * Begin transaction.
	 */
	public function beginTransaction()
	{
		$this->connect();
		$this->doBeginTransaction();
	}

	/**
	 * Commit transaction
	 */
	public function commit()
	{
		$this->connect();
		$this->doCommit();
	}

	/**
	 * Roll back transaction
	 */
	public function rollBack()
	{
		$this->connect();
		$this->doRollBack();
	}

	/**
	 * Seelect table rows
	 *
	 * @param string $table
	 * @param mixed $columns
	 * @param mixed $where
	 * @param mixed $order
	 * @param int $count
	 * @param int $offset
	 * @return Eka_Db_Statement_Abstract
	 */
	public function select($table, $columns = null, $where = null, $order = null, $count = null, $offset = null)
	{
		$where = $this->prepareWhere($where);
		if (null !== $where) {
			$bind = $where[1];
			$where = ' WHERE ' . $where[0];
		} else {
			$bind = null;
		}

		$order = $this->prepareOrder($order);
		if (null !== $order) {
			$order = ' ORDER BY ' . $order;
		}

		$sql = 'SELECT ' . $this->prepareColumns($columns)
			. ' FROM ' . $this->quoteIdentifier($table)
			. $where . $order;

		$sql = $this->limit($sql, $count, $offset);

		return $this->query($sql, $bind);
	}

	/**
	 * Inserts a table row
	 *
	 * @param string $table
	 * @param array $data Array of column => value
	 * @return int Last insert id, if possible
	 */
	public function insert($table, array $data)
	{
		$cols = array();
		$vals = array();
		foreach ($data as $col => $val) {
			$cols[] = $this->quoteIdentifier($col);
			if ($val instanceof Eka_Db_Expr) {
				$vals[] = $val->toString();
				unset($data[$col]);
			} else {
				$vals[] = '?';
			}
		}

		$sql = 'INSERT INTO ' . $this->quoteIdentifier($table)
			. ' (' . implode(', ', $cols) . ')'
			. ' VALUES (' . implode(', ', $vals) . ')';

		$this->query($sql, array_values($data));
		return $this->getLastInsertId();
	}

	/**
	 * Updates table rows
	 *
	 * @param string $table
	 * @param array $data Array of column => value
	 * @param mixed $where
	 * @param mixed $order
	 * @param int $count
	 * @param int $offset
	 * @return int The number of affected rows
	 */
	public function update($table, array $data, $where = null, $order = null, $count = null, $offset = null)
	{
		$set = array();
		foreach ($data as $col => $val) {
			if ($val instanceof Eka_Db_Expr) {
				$val = $val->toString();
				unset($data[$col]);
			} else {
				$val = '?';
			}
			$set[] = $this->quoteIdentifier($col) . ' = ' . $val;
		}

		$bind = array_values($data);
		unset($data);

		$where = $this->prepareWhere($where);
		if (null !== $where) {
			$bind = array_merge($bind, $where[1]);
			$where = ' WHERE ' . $where[0];
		}

		$order = $this->prepareOrder($order);
		if (null !== $order) {
			$order = ' ORDER BY ' . $order;
		}

		$sql = 'UPDATE ' . $this->quoteIdentifier($table)
			. ' SET ' . implode(', ', $set)
			. $where . $order;

		$sql = $this->limit($sql, $count, $offset);

		$stmt = $this->query($sql, $bind);
		$result = $stmt->rowCount();
		return $result;
	}

	/**
	 * Deletes table rows
	 *
	 * @param mixed $table
	 * @param mixed $where
	 * @param mixed $order
	 * @param int $count
	 * @param int $offset
	 * @return int Number of affected rows
	 */
	public function delete($table, $where = null, $order = null, $count = null, $offset = null)
	{
		$where = $this->prepareWhere($where);
		if (null !== $where) {
			$bind = $where[1];
			$where = ' WHERE ' . $where[0];
		} else {
			$bind = null;
		}

		$order = $this->prepareOrder($order);
		if (null !== $order) {
			$order = ' ORDER BY ' . $order;
		}

		$sql = 'DELETE FROM ' . $this->quoteIdentifier($table)
			. $where . $order;

		$sql = $this->limit($sql, $count, $offset);

		$stmt = $this->query($sql, $bind);
		$result = $stmt->rowCount();
		return $result;
	}

	/**
	 * Prepare order
	 *
	 * @todo РЅР°РїРёС€Рё РЅРѕСЂРјР°Р»СЊРЅРѕРµ РѕРїРёСЃР°РЅРёРµ
	 *
	 * @param mixed $order
	 * @return string
	 */
	protected function prepareOrder($order)
	{
		if (empty($order)) {
			return null;
		}

		if (is_string($order)) {
			$order = explode(',', $order);
		}
		$result = array();
		foreach ($order as $key => $val) {
			if ($val instanceof Eka_Db_Expr) {
				$val = $val->toString();
			} elseif (is_string($key)) {
				$v = strtoupper($val);
				if ($v == 'DESC' || $v == 'ASC') {
					$val = $key . ' ' . $v;
				} else {
					include_once 'Eka/Db/Adapter/Exception.php';
					throw new Eka_Db_Adapter_Exception("Bad order argument '$key' => '$v'");
				}
			}
			$result[] = $val;
		}

		return implode(', ', $result);
	}

	/**
	 * Prepare columns
	 *
	 * @todo РЅР°РїРёС€Рё РЅРѕСЂРјР°Р»СЊРЅРѕРµ РѕРїРёСЃР°РЅРёРµ
	 *
	 * @param mixed $columns
	 * @return string
	 */
	protected function prepareColumns($columns)
	{
		if (null === $columns || (is_string($columns) && '*' === trim($columns))) {
			return '*';
		}

		$result = array();
		if (is_string($columns)) {
			$columns = explode(',', $columns);
		} elseif (!is_array($columns)) {
			$columns = array($columns);
		}
		foreach ($columns as $as => $column) {
			if ($column instanceof Eka_Db_Expr) {
				$column = $column->toString();
			} else {
				$column = $this->quoteIdentifier(trim($column));
			}
			if (is_string($as)) {
				$column .= ' AS ' . $this->quoteIdentifier($as);
			}
			$result[] = $column;
		}
		return implode(', ', $result);
	}

	/**
	 * Prepare where
	 *
	 * @todo РЅР°РїРёС€Рё РЅРѕСЂРјР°Р»СЊРЅРѕРµ РѕРїРёСЃР°РЅРёРµ
	 *
	 * @param mixed $where
	 * @return array
	 */
	protected function prepareWhere($where)
	{
		if (empty($where)) {
			return null;
		}

		$terms = array();
		$bind = array();
		if (is_array($where)) {
			foreach ($where as $term => $val) {
				if (is_array($val)) {
					//if (!is_string($term)) #TODO РЅР°РґРѕ РіРµРЅРµСЂРёС‚СЊ РѕС€РёР±РєСѓ
					foreach ($val as $k => $v) {
						if (is_int($k)) {
							$bind[] = $v;
						} else {
							$bind[$k] = $v;
						}
					}
				} elseif (is_string($term)) {
					if (strpos($term, '?') === false) {
						$term .= '=?';
					}
					$bind[] = $val;
				} else {
					if ($val instanceof Eka_Db_Expr) {
						$val = $val->toString();
					}
					$term = $val;
				}
				//$terms[] = '(' . $term . ')';
				$terms[] = $term;
			}
			if (count($terms) > 1) {
				foreach ($terms as &$v) {
					$v = '(' . $v . ')';
				}
			}
			return array(implode(' AND ', $terms), $bind);
		}
		return array($where, $bind);
	}

	/**
	 * Returns all rows from the result set as an array indexed by both column name and 0-indexed column number
	 *
	 * @param  string $sql
	 * @param  mixed
	 * @return array Collection of rows
	 */
	public function fetch($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetchAll();
	}

	/**
	 * Returns all rows from the result set as an array indexed by column name
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array Collection of rows
	 */
	public function fetchAssoc($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetchAllAssoc();
	}

	/**
	 * Returns all rows from the result set as an array indexed by column number, starting at column 0
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array Collection of rows
	 */
	public function fetchNum($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetchAllNum();
	}

	/**
	 * @param string $sql
	 * @param mixed $bind
	 * @param int $pos
	 * @return array Collection of rows
	 */
	public function fetchColumn($sql, $bind = null, $pos = 0)
	{
		return $this->query($sql, $bind)->fetchAllColumn($pos);
	}

	/**
	 * Fetches all SQL result rows as an array of key-value pairs
	 *
	 * The first column is the key, the second column is the value.
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array
	 */
	public function fetchPairs($sql, $bind = null)
	{
		$stmt = $this->query($sql, $bind);
		$result = array();
		while (($row = $stmt->fetchNum())) {
			$result[$row[0]] = $row[1];
		}
		return count($result) > 0 ? $result : null;
	}

	/**
	 * Fetches the first row of the SQL result as an array indexed by both column name and 0-indexed column number
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array
	 */
	public function fetchOne($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetch();
	}

	/**
	 * Fetches a row from the result set as an array indexed by column name
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array
	 */
	public function fetchOneAssoc($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetchAssoc();
	}

	/**
	 * Fetches a row from the result set as an array indexed by column number, starting at column 0
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @return array
	 */
	public function fetchOneNum($sql, $bind = null)
	{
		return $this->query($sql, $bind)->fetchNum();
	}

	/**
	 * Fetches column for $pos of the first row of the SQL result
	 *
	 * @param string $sql
	 * @param mixed $bind
	 * @param int $pos
	 * @return mixed
	 */
	public function fetchCell($sql, $bind = null, $pos = 0)
	{
		$column = $this->query($sql, $bind)->fetchColumn($pos);
		return $column;
	}

	/**
	 * Safely quotes a value for an SQL statement.
	 *
	 * @param mixed $value
	 * @return mixed
	 */
	public function quote($value)
	{
		if ($value instanceof Eka_Db_Expr) {
			return $value->toString();
		}

		if (is_array($value)) {
			foreach ($value as &$val) {
				$val = $this->quote($val);
			}
			return $value;
		}

		return $this->doQuote($value);
	}

	/**
	 * Quotes a value and places into a piece of text at a placeholder
	 *
	 * The placeholder is a question-mark; all placeholders will be replaced
	 * with the quoted value
	 *
	 * @param string $text
	 * @param mixed  $value
	 * @return string
	 */
	public function quoteInto($text, $value)
	{
		if (is_array($value)) {
			foreach ($value as $value) {
				$text = preg_replace('`\?`', $this->quote($value), $text, 1);
			}
			return $text;
		}
		return str_replace('?', $this->quote($value), $text);
	}

	/**
	 * Quotes an identifier
	 *
	 * @param string $ident
	 * @return string
	 */
	public function quoteIdentifier($ident)
	{
		$q = $this->quoteIdentifierSymbol;
		$ident = str_replace($q, $q.$q, $ident);
		$ident = $q . str_replace('.', $q . '.' . $q, $ident) . $q;
		return $ident;
	}

	public function unQuoteIdentifier($ident)
	{
		$q = $this->quoteIdentifierSymbol;
		$q = preg_quote($q);
		$ident = preg_replace(array('/^'.$q.'/', '/'.$q.'$/', '/'.$q.'\.'.$q.'/', '/'.$q.$q.'/'), array('', '', '.', '`'), $ident);
		return $ident;
	}

	# Abstract Methods
	############################################################################

	/**
	 * Quote a raw string
	 *
	 * @param string $value
	 * @return string
	 */
	abstract protected function doQuote($value);
	
	/**
	 * Creates a connection to the database
	 *
	 * @return void
	 */
	abstract protected function connect();

	/**
	 * Begin a transaction
	 */
	abstract protected function doBeginTransaction();

	/**
	 * Commit a transaction
	 */
	abstract protected function doCommit();

	/**
	 * Roll-back a transaction
	 */
	abstract protected function doRollBack();

	/**
	 * Force the connection to close
	 *
	 * @return void
	 */
	abstract public function closeConnection();

	/**
	 * Prepare a statement and return a Statement object
	 *
	 * @param string $sql
	 * @return Eka_Db_Statement_Abstract
	 */
	abstract public function prepare($sql);

	/**
	 * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column
	 *
	 * @return string
	 */
	abstract public function getLastInsertId();

	/**
	 * Returns the column descriptions for a table
	 *
	 * columns: array of column names =>
	 *     name => string; column name
	 *     position => number; ordinal position of column in table
	 *     type => string; SQL datatype name of column
	 *     default => string; default expression of column, null if none
	 *     null => boolean; true if column can have nulls
	 *     length => number; length of CHAR/VARCHAR
	 *     scale => number; scale of NUMERIC/DECIMAL
	 *     precision => number; precision of NUMERIC/DECIMAL
	 *     unsigned => boolean; unsigned property of an integer type
	 *     primary => boolean; true if column is part of the primary key
	 *     primary_position => integer; position of column in primary key
	 *     identity => integer; true if column is auto-generated with unique values
	 * foreign_keys: array of key names:
	 *     table => string; reference table name
	 *     foreign => array; columns name
	 *     reference => array; reference columns name
	 *
	 * @param string $tableName
	 * @param string $tableSchema
	 * @return array
	 */
	abstract public function describeTable($tableName, $tableSchema = null);

	/**
	 * Adds an adapter-specific LIMIT clause to the SELECT statement
	 *
	 * @param mixed $sql
	 * @param integer $count
	 * @param integer $offset
	 * @return string
	 */
	abstract public function limit($sql, $count = null, $offset = 0);
}
